JavaScript 学习笔记之对象的创建


1、Object 构建函数

在 JavaScript 中创建对象最简单的方法就是创建一个 object 的实例,然后为它添加所需的属性和方法。

1
2
3
4
5
6
7
8
//Object 构建函数方法
var person = new Object();
person.name = 'Eyesim';
person.age = 22;
person.job = 'developer';
person.sayName = function() {
alert(this.name);
};

这种 Object 构建函数的方法在早期比较流行,后来对象字面量的方法成为创建字面量的首选。

2、对象字面量

1
2
3
4
5
6
7
8
9
//对象字面量
var person = {
name:'Eyesim',
age:22,
job:'developer',
sayName: function() {
alert(this.name);
}
}

这两个方法创建单个对象还是挺方便的,但是它们在用于创建多个类似的对象时就会显得特别繁琐,只能一个又一个地重复去创建,而且类似的对象之间也没法看出有什么联系,此时就出现了以下几种常见的创建对象的模式:
首先是在上面的模式上改进的模式,可以理解为工厂模式

3、工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//工厂模式
function Person (name,age,job) {
var p = new Object();
p.name = name;
p.age = age;
p.job = job;
p.sayName = function() {
alert(this.name);
};
return p;
}
var eyesim = Person ('Eyesim',22,'developer');
var gordon = Person ('Gordon',22,'engineer');
eyesim.constructor === Person;//false
gordon.constructor === Person;//false
eyesim.constructor === Object;//true
gordon.constructor === Object;//true
eyesim instanceof Object; //true
eyesim instanceof Person; //false

利用工厂模式的方法可以成功创建多个类似的对象,但是这种方法有个缺陷是,eyesim.constructor 返回的是根部的 Object,用 instanceof 操作符去判断实例类型的时候 Person 的判断也是错的,也就是说这种模式无法知道到底是创建的这些对象是谁的实例。于是就出现了另一种方法——构造函数模式.

4、构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//构造函数模式
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
alert(this.name);
};
}
var eyesim = new Person ('Eyesim',22,'developer');
var gordon = new Person ('Gordon',22,'engineer');
eyesim.constructor === Person;//true
gordon.constructor === Person;//true
eyesim.constructor === Object;//false
gordon.constructor === Object;//false
eyesim instanceof Object; //true
eyesim instanceof Person; //true

这种方法解决了工厂模式下,实例与构造函数间的联系的问题,但是这种方法并不是很完美的,因为他在创建好的实例里面会重新定义方法:

1
eyesim.sayName === gordon.sayName;//false

这点在多个实例上就会显得特别占内存,那么对于那些实例间共有的方法与属性,可以怎么解决呢?我们可以对这个方法改进一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//构造函数模式的改进
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
};
function sayName() {
alert(this.name);
};
var eyesim = new Person ('Eyesim',22,'developer');
var gordon = new Person ('Gordon',22,'engineer');
eyesim.constructor === Person;//true
gordon.constructor === Person;//true
eyesim.constructor === Object;//false
gordon.constructor === Object;//false
eyesim instanceof Object; //true
eyesim instanceof Person; //true
eyesim.sayName === gordon.sayName;//true

这种方法通过把函数的定义转移到构造函数的外面来解决,此时内部的 sayName 就被设置为全局的 sayName 函数,这样一来 sayName 包含的是一个指向函数的指针,因此 eyesim 与 gordon 就共享了在全局域中所定义的同一个 sayName() 函数,这样就解决了让两个函数做同一件事的问题,但是呢,这个会存在什么问题?

1.在全局域中定义的函数只能被某个对象调用这让全局作用域有点名不其实
2.如果对象需要很多种共有的方法的话,这是不是得在全局域定义不同的函数?

那么,还有什么创建对象的模式可以解决这些问题呢?幸好我们还有原型模式

5、原型模式

其实我们创建的每一个函数都有一个 prototype (原型)的属性,这个属性是一个指针,指向一个对象,这个对象的用途就是可以包含由特定类型的所有实例共享的属性和方法,也就是说 prototype 就是通过构建函数创建的那个对象实例的原型对象,使用这个原型对象的好处就是可以让所有的对象实例共享它所含有的属性和方法,利用这个属性,我们大致就可以解决上面构造函数模式遇到的共享的问题。首先我们来创建这个原型对象:

1
2
3
4
5
6
7
8
9
10
function Person (name,age,job){
this.name = name;
this.age = age;
this.job = job;
};
Person.prototype.sayName = function (){
alert(this.name);
};
Person.prototype.constructor === Object;//true
Person.prototype.constructor === Person;//true

我们也可以用字面量方法创建:

1
2
3
4
5
6
7
8
9
10
11
12
function Person (name,age,job){
this.name = name;
this.age = age;
this.job = job;
};
Person.prototype = {
sayName: function (){
alert(this.name);
}
};
var eyesim = new Person ('Eyesim',22,'developer');
var gordon = new Person ('Gordon',22,'engineer');

字面量重写原型对象的方法虽然能够满足共享方法与属性的问题,但是存在这一个很大的问题,是什么呢?

1
2
Person.prototype.constructor === Object;//true
Person.prototype.constructor === Person;//false

这是怎么回事呢,原来原型对象在一开始创建的时候会默认有一个 constructor 的属性指向构造函数 Person ,但是在用字面量方式创建的时候会将原型对象重写,反而导致了原型对象失去了与构造函数的联系,这时候我们可以在字面量创建对象的时候可以通过给原型对象添加一个 constructor 属性修补这一个关系的桥梁:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person (name,age,job){
this.name = name;
this.age = age;
this.job = job;
};
Person.prototype = {
constructor: Person,
sayName: function (){
alert(this.name);
}
};
var eyesim = new Person ('Eyesim',22,'developer');
var gordon = new Person ('Gordon',22,'engineer');

说完这一点要注意的,我们继续说原型模式,原型模式是不是完美的呢?当然不是,再美好的东西都是存在缺点的,那原型模式存在什么问题呢?下面我一点点来说。
根据 JavaScript 的基本概念,ECMAScript 变量可能包含两种不同类型的值:基本类型值与引用类型值,在将一个值赋值给变量的时候,解析器必须确定这个值是基本数据类型值还是引用类型值,那么这两种类型值有什么不同呢?在这里,我们最主要说的是,基本数据类型值是按值访问的,而引用类型值是按引用访问的,也就是说,当赋值给原型对象时,如果属性类型是基本数据类型的话还好,赋值是什么,表现就是什么,不同的实例可以有不同的值,但是如果是引用类型的话,会发生什么事情呢?后面赋值的会覆盖掉前面的赋值,为什么呢?因为对于引用类型来说,当保存着对象的某个变量时,操作的是对象的引用,但在为对象添加属性时,操作的是实际的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person (name,age,job){
this.name = name;
this.age = age;
this.job = job;
};
Person.prototype = {
constructor: Person,
sex:'female',
sayName: function (){
alert(this.name);
}
};
var eyesim = new Person ('Eyesim',22,'developer');
var gordon = new Person ('Gordon',22,'engineer');
gordon.sex = 'male';
gordon.sex === 'male';//true
eyesim.sex === 'female';//true
gordon.constructor.prototype.sex === 'female';//true
eyesim.constructor.prototype.sex === 'female';//true

在这里面,sex 的数据类型为基本数据类型,所以修改 gordon.sex 的时候并不会影响到原型的 sex 的值,也不会影响到其他实例的属性值

1
2
3
4
5
6
7
8
gordon.palce[0] = 'Beijing';
gordon.palce[0] === 'Beijing';//true
eyesim.palce[0] === 'Guangzhou';//false
eyesim.palce[0] === 'Beijing';//true
gordon.constructor.prototype.palce[0] === 'Guangzhou';//false
eyesim.constructor.prototype.palce[0] === 'Guangzhou';//false
gordon.constructor.prototype.palce[0] === 'Beijing';//true
eyesim.constructor.prototype.palce[0] === 'Beijing';//true

这里的 palce 为引用类型值,当实例修改了这种引用类型的属性时,原型对象里面的属性值就被重写了,连其他实例也跟着变,这是因为存储在原型对象中的是该引用类型变量的地址,于是在所有实例对象中该属性的地址都指向同一个数组,所以当任何一个实例对象对该引用类型的变量进行修改,所有引用了这一变量的值都跟着变了,这就是原型对象里面不好的一点。

6、构造函数与原型混成的模式

为了避免以上这几种模式的各种缺点,我们可以使用多种模式组合的方式,其中构造函数模式与原型模式的组合最常见了,话不多说,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.place = ['Guangzhou','Shenzhen'];
};
Person.prototype = {
constructor:Person,
sayName: function(){
alert(this.name);
}
};
var eyesim = new Person ('Eyesim',22,'developer');
var gordon = new Person ('Gordon',22,'engineer');
eyesim.place.push('Beijing');
eyesim.place[2]==='Beijing';//true
gordon.place[2]==='Beijing';//false

这种方法实例属性都是在构造函数里面定义,而由所有实例共享的属性 consturctor 与方法则是在原型对象中定义,而修改了 eyesim.place 以后并不影响其他实例对象的值,因为它们分别引用的是不同的数组。目前这种构造函数与原型混成的模式,是在 ECMAScript 中使用得最广泛、认同度最高的一种创建自定义类型的方法,可以说,这是用来定义引用类型的一种默认模式。

7、动态原型模式

如果说,对独立的构造函数和原型看不惯的话,可以试一下动态原型模型,它把所有的信息都封装在了构造函数中,而通过在构造函数中初始化原型(必要才这么做),又保持了同时使用构造函数和原型的优点,我们可以根据检查某个应该存在的方法是否有效来决定是否需要初始化原型,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
function Person (name,age,job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this sayName != 'function') {
Person.prototype.sayName = function() {
alert(this.name);
};
}
};
var eyesim = new Person ('Eyesim',22,'developer');
eyesim.sayName();

8、寄生构造函数模式

通常,在以上几种模式都不适用的情况下,我们可以考虑一下寄生构造函数模式,这个模式就是创建一个函数,而这个函数仅仅是为了封装,然后返回新创建的对象,继续上代码:

1
2
3
4
5
6
7
8
9
10
11
12
function Person (name,age,job){
var o = new Object(();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
};
return o;
}
var eyesim = new Person ('Eyesim',22,'developer');
eyesim.sayName();//Eyesim

这种方法看起来超级像是工厂模式与构造函数模式的结合,但假设我们想要创建一个具有额外方法的特殊数组,这个模是怎么做的呢?

1
2
3
4
5
6
7
8
9
10
function specialArr () {
var values = new Array();
values.push.apply(values,arguments);
values.pipedString = function () {
return this.join("|");
};
return values;
}
var arr = new specialArr(1,2,3);
console.log(arr.pipedString()); //输出 1|2|3

这个方法我们创建并初始化了了 arr 数组为 [1,2,3],通过方法 pipedString 返回以竖线为分割的数组值。对于寄生构造函数模式,这里有一点需要说明的,第一,返回的对象与构造函数或者与构造函数的原型属性之间是没有联系的,也就是说构造函数返回的对象与在构造函数外部创建的对象没有什么不同。因此用 instanceof 操作符是不能确定对象类型的,所以一般情况下,不建议使用这种模式。

9、稳妥构造函数模式

稳妥对象指的是那些没有公共属性而且其方法也不引用 this 的对象,它最适合在一些安全的环境中,或者在防止数据被其他应用程序改动时使用。稳妥构造函数模式与寄生构造函数类似但又有两点不同:一是新创建对象的实例方法不引用 this ;二是不使用 new 操作符调用构造函数。

1
2
3
4
5
6
7
8
9
function Person (name,age,job) {
var o = new Object();
o.sayName = function () {
alert(name);
}
return o;
}
var eyesim = Person ('Eyesim',22,'developer');
eyesim.sayName();//eyesim

这种模式下创建的对象中除了使用 sayName 函数,谁也无法访问 name 属性,即使有代码会给这个对象添加方法或数据成员,但也不可能有别的方法可以访问到构造函数中原始的数据,这种安全性就是该模式最大的特点。但同样要注意的是,稳妥构造函数模式跟寄生构造函数模式一样,创建的对象都无法确定其对象类型。